Repository navigation
Fix IPv6 address with zone ID SSRF bypass - #445
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| def parse_address(address) | ||
| return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address) | ||
|
|
||
| Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST) | ||
| .map { |info| IPAddr.new(info[3]) } | ||
| Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST).map do |info| | ||
| # `IPAddr.new` only accepts a zone ID on Ruby 3.1+. | ||
| IPAddr.new(info[3].partition("%").first) | ||
| end | ||
| rescue SocketError | ||
| nil | ||
| end |
There was a problem hiding this comment.
🟠 High - RFC 6874 encoded zone IDs still bypass private-IP detection
A caller sends a valid RFC 6874 URL such as http://[fe80::1%25lo] through a supported HTTP sink. The URI hostname retains the encoded %25, but parse_address forwards it unchanged to getaddrinfo; when that scope lookup fails, resolution falls through without checking the underlying IPv6 address. The scanner therefore treats a link-local/private IPv6 target as external and permits an SSRF when the HTTP client accepts and connects the encoded URL.
Show fix
| def parse_address(address) | |
| return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address) | |
| Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST) | |
| .map { |info| IPAddr.new(info[3]) } | |
| Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST).map do |info| | |
| # `IPAddr.new` only accepts a zone ID on Ruby 3.1+. | |
| IPAddr.new(info[3].partition("%").first) | |
| end | |
| rescue SocketError | |
| nil | |
| end | |
| def parse_address(address) | |
| return nil unless address.is_a?(String) && ADDRESS_REGEXP.match?(address) | |
| address = address.sub(/%25/i, "%") | |
| Socket.getaddrinfo(address, nil, :UNSPEC, :STREAM, nil, Socket::AI_NUMERICHOST).map do |info| | |
| # `IPAddr.new` only accepts a zone ID on Ruby 3.1+. | |
| IPAddr.new(info[3].partition("%").first) | |
| end | |
| rescue SocketError | |
| nil | |
| end |
More info - Reply on this comment to give feedback or ignore the issue.
There was a problem hiding this comment.
The builtin URI class does not implement RFC 6874, and raises URI::InvalidURIError when given a URI containing an IPv6 address with a zone ID to parse. How best to accommodate other URI implementations is currently being considered.
This change fixes an SSRF bypass; a private IPv6 address with a zone ID (i.e.
fe80::1%eth0) is not recognized as a private IP address and is treated as a hostname byAikido::Zen::Scanners::SSRF::PrivateIPChecker. The private IP checker now recognizes IPv6 addresses with an optional zone ID as IP addresses, and removes the zone ID before building anIPAddr(IPAddr.newbefore Ruby 3.1 raises when given a zone ID). The zone ID does not affect which range an IP address is in.